import { Logger } from 'util/logger';

const logger = new Logger('otp');

const Otp = function (url, params) {
    if (['hotp', 'totp'].indexOf(params.type) < 0) {
        throw 'Bad type: ' + params.type;
    }
    if (!params.secret) {
        throw 'Empty secret';
    }
    if (params.algorithm && ['SHA1', 'SHA256', 'SHA512'].indexOf(params.algorithm) < 0) {
        throw 'Bad algorithm: ' + params.algorithm;
    }
    if (params.digits && ['6', '7', '8'].indexOf(params.digits) < 0) {
        throw 'Bad digits: ' + params.digits;
    }
    if (params.type === 'hotp' && !params.counter) {
        throw 'Bad counter: ' + params.counter;
    }
    if ((params.period && isNaN(params.period)) || params.period < 1) {
        throw 'Bad period: ' + params.period;
    }

    this.url = url;

    this.type = params.type;
    this.account = params.account;
    this.secret = params.secret;
    this.issuer = params.issuer;
    this.algorithm = params.algorithm ? params.algorithm.toUpperCase() : 'SHA1';
    this.digits = params.digits ? +params.digits : 6;
    this.counter = params.counter;
    this.period = params.period ? +params.period : 30;

    this.key = Otp.fromBase32(this.secret);
    if (!this.key) {
        throw 'Bad key: ' + this.key;
    }
};

Otp.prototype.next = function (callback) {
    let valueForHashing;
    let timeLeft;
    if (this.type === 'totp') {
        const now = Date.now();
        const epoch = Math.round(now / 1000);
        valueForHashing = Math.floor(epoch / this.period);
        const msPeriod = this.period * 1000;
        timeLeft = msPeriod - (now % msPeriod);
    } else {
        valueForHashing = this.counter;
    }
    const data = new Uint8Array(8).buffer;
    new DataView(data).setUint32(4, valueForHashing);
    this.hmac(data, (sig, err) => {
        if (!sig) {
            logger.error('OTP calculation error', err);
            return callback(err);
        }
        sig = new DataView(sig);
        const offset = sig.getInt8(sig.byteLength - 1) & 0xf;
        const hmac = sig.getUint32(offset) & 0x7fffffff;
        let pass;
        if (this.issuer === 'Steam') {
            pass = Otp.hmacToSteamCode(hmac);
        } else {
            pass = Otp.hmacToDigits(hmac, this.digits);
        }
        callback(null, pass, timeLeft);
    });
};

Otp.prototype.hmac = function (data, callback) {
    const subtle = window.crypto.subtle || window.crypto.webkitSubtle;
    const algo = { name: 'HMAC', hash: { name: this.algorithm.replace('SHA', 'SHA-') } };
    subtle
        .importKey('raw', this.key, algo, false, ['sign'])
        .then((key) => {
            subtle
                .sign(algo, key, data)
                .then((sig) => {
                    callback(sig);
                })
                .catch((err) => {
                    callback(null, err);
                });
        })
        .catch((err) => {
            callback(null, err);
        });
};

Otp.hmacToDigits = function (hmac, length) {
    let code = hmac.toString();
    code = Otp.leftPad(code.substr(code.length - length), length);
    return code;
};

Otp.hmacToSteamCode = function (hmac) {
    const steamChars = '23456789BCDFGHJKMNPQRTVWXY';
    let code = '';
    for (let i = 0; i < 5; ++i) {
        code += steamChars.charAt(hmac % steamChars.length);
        hmac /= steamChars.length;
    }
    return code;
};

Otp.fromBase32 = function (str) {
    str = str.replace(/\s/g, '');
    const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
    let bin = '';
    let i;
    for (i = 0; i < str.length; i++) {
        const ix = alphabet.indexOf(str[i].toLowerCase());
        if (ix < 0) {
            return null;
        }
        bin += Otp.leftPad(ix.toString(2), 5);
    }
    const hex = new Uint8Array(Math.floor(bin.length / 8));
    for (i = 0; i < hex.length; i++) {
        const chunk = bin.substr(i * 8, 8);
        hex[i] = parseInt(chunk, 2);
    }
    return hex.buffer;
};

Otp.leftPad = function (str, len) {
    while (str.length < len) {
        str = '0' + str;
    }
    return str;
};

Otp.parseUrl = function (url) {
    const match = /^otpauth:\/\/(\w+)(?:\/([^\?]+)\?|\?)(.*)/i.exec(url);
    if (!match) {
        throw 'Not OTP url';
    }
    const params = {};
    const label = decodeURIComponent(match[2] ?? 'default');
    if (label) {
        const parts = label.split(':');
        params.issuer = parts[0].trim();
        if (parts.length > 1) {
            params.account = parts[1].trim();
        }
    }
    params.type = match[1].toLowerCase(); // returns "totp"
    // match[3] =  secret=XXXXXXXXXXXXX&period=30&digits=6&algorithm=SHA1
    match[3].split('&').forEach((part) => {
        const parts = part.split('=', 2);
        params[parts[0].toLowerCase()] = decodeURIComponent(parts[1]);
    });
    return new Otp(url, params);
};

Otp.isSecret = function (str) {
    return !!Otp.fromBase32(str);
};

Otp.makeUrl = function (secret, period, digits) {
    return (
        'otpauth://totp/default?secret=' +
        secret +
        (period ? '&period=' + period : '') +
        (digits ? '&digits=' + digits : '')
    );
};

export { Otp };
